summaryrefslogtreecommitdiff
path: root/app/[lng]
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]')
-rw-r--r--app/[lng]/partners/(partners)/dolce-upload-v3/dolce-upload-page-v3.tsx585
-rw-r--r--app/[lng]/partners/(partners)/dolce-upload-v3/page.tsx49
2 files changed, 634 insertions, 0 deletions
diff --git a/app/[lng]/partners/(partners)/dolce-upload-v3/dolce-upload-page-v3.tsx b/app/[lng]/partners/(partners)/dolce-upload-v3/dolce-upload-page-v3.tsx
new file mode 100644
index 00000000..513cfe1e
--- /dev/null
+++ b/app/[lng]/partners/(partners)/dolce-upload-v3/dolce-upload-page-v3.tsx
@@ -0,0 +1,585 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import { useState, useEffect, useCallback, useMemo } from "react";
+import { useParams } from "next/navigation";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { InfoIcon, RefreshCw, Search, Upload, Plus, Cloud } from "lucide-react";
+import { toast } from "sonner";
+import { useTranslation } from "@/i18n/client";
+import {
+ UnifiedDwgReceiptItem,
+ DetailDwgReceiptItem,
+ FileInfoItem,
+ fetchDwgReceiptList,
+ getVendorSessionInfo,
+ fetchVendorProjects,
+ fetchDetailDwgReceiptListV2,
+ fetchFileInfoListV2,
+ fetchPendingSyncItems,
+ deleteLocalFile,
+} from "@/lib/dolce-v2/actions";
+import { DrawingListTableV2 } from "@/lib/dolce/table/drawing-list-table-v2";
+import { drawingListColumns } from "@/lib/dolce/table/drawing-list-columns";
+import { createGttDrawingListColumns, DocumentType } from "@/lib/dolce/table/gtt-drawing-list-columns";
+import { createDetailDrawingColumns } from "@/lib/dolce/table/detail-drawing-columns";
+import { createFileListColumns } from "@/lib/dolce/table/file-list-columns";
+
+// 다이얼로그 (V2/V3)
+import { B4BulkUploadDialogV3Sync } from "@/lib/dolce-v2/dialogs/b4-bulk-upload-dialog-v3";
+import { AddAndModifyDetailDrawingDialogV2 } from "@/lib/dolce-v2/dialogs/add-and-modify-detail-drawing-dialog-v2";
+import { UploadFilesToDetailDialogV2 } from "@/lib/dolce-v2/dialogs/upload-files-to-detail-dialog-v2";
+import { SyncItemsDialog } from "@/lib/dolce-v2/dialogs/sync-items-dialog";
+
+interface DolceUploadPageV3Props {
+ searchParams: { [key: string]: string | string[] | undefined };
+}
+
+export default function DolceUploadPageV3({ searchParams }: DolceUploadPageV3Props) {
+ const params = useParams();
+ const lng = params?.lng as string;
+ const { t } = useTranslation(lng, "dolce");
+
+ // URL에서 초기 프로젝트 코드
+ const initialProjNo = (searchParams.projNo as string) || "";
+
+ // 상태 관리
+ const [drawings, setDrawings] = useState<UnifiedDwgReceiptItem[]>([]);
+ const [projects, setProjects] = useState<Array<{ code: string; name: string }>>([]);
+ const [vendorInfo, setVendorInfo] = useState<{
+ userId: string;
+ userName: string;
+ email: string;
+ vendorCode: string;
+ vendorName: string;
+ drawingKind: "B3" | "B4";
+ } | null>(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ // 필터 상태
+ const [projNo, setProjNo] = useState(initialProjNo);
+ const [drawingNo, setDrawingNo] = useState("");
+ const [drawingName, setDrawingName] = useState("");
+ const [discipline, setDiscipline] = useState("");
+ const [manager, setManager] = useState("");
+ const [documentType, setDocumentType] = useState<DocumentType>("ALL"); // B4 전용
+
+ // 선택된 도면 및 상세도면
+ const [selectedDrawing, setSelectedDrawing] = useState<UnifiedDwgReceiptItem | null>(null);
+ const [detailDrawings, setDetailDrawings] = useState<DetailDwgReceiptItem[]>([]);
+ const [selectedDetail, setSelectedDetail] = useState<DetailDwgReceiptItem | null>(null);
+ const [files, setFiles] = useState<FileInfoItem[]>([]);
+ const [isLoadingDetails, setIsLoadingDetails] = useState(false);
+ const [, setIsLoadingFiles] = useState(false);
+
+ // 다이얼로그 상태
+ const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = useState(false);
+ const [addDialogOpen, setAddDialogOpen] = useState(false);
+ const [uploadFilesDialogOpen, setUploadFilesDialogOpen] = useState(false);
+ const [syncDialogOpen, setSyncDialogOpen] = useState(false);
+
+ // 동기화 상태
+ const [pendingSyncCount, setPendingSyncCount] = useState(0);
+
+ // 미동기화 건수 확인
+ const checkPendingSync = useCallback(async () => {
+ if (!projNo || !vendorInfo) return;
+ try {
+ const items = await fetchPendingSyncItems({ projectNo: projNo, userId: vendorInfo.userId });
+ setPendingSyncCount(items.length);
+ } catch (e) {
+ console.error("Failed to check pending sync items", e);
+ }
+ }, [projNo, vendorInfo]);
+
+ // 초기 데이터 로드
+ const loadInitialData = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ const [vendorInfoData, projectsData] = await Promise.all([
+ getVendorSessionInfo(),
+ fetchVendorProjects(),
+ ]);
+
+ setVendorInfo(vendorInfoData as typeof vendorInfo);
+ setProjects(projectsData);
+
+ if (initialProjNo) {
+ const drawingsData = await fetchDwgReceiptList({
+ project: initialProjNo,
+ drawingKind: vendorInfoData.drawingKind,
+ drawingVendor: vendorInfoData.drawingKind === "B3" ? vendorInfoData.vendorCode : "",
+ });
+ setDrawings(drawingsData);
+ }
+ } catch (err) {
+ console.error("초기 데이터 로드 실패:", err);
+ setError(err instanceof Error ? err.message : t("page.initialLoadError"));
+ toast.error(t("page.initialLoadError"));
+ } finally {
+ setIsLoading(false);
+ }
+ }, [initialProjNo, t]);
+
+ // 도면 목록 조회
+ const loadDrawings = useCallback(async () => {
+ if (!projNo || !vendorInfo) return;
+
+ try {
+ setIsRefreshing(true);
+ setError(null);
+
+ const drawingsData = await fetchDwgReceiptList({
+ project: projNo,
+ drawingKind: vendorInfo.drawingKind,
+ drawingVendor: vendorInfo.drawingKind === "B3" ? vendorInfo.vendorCode : "",
+ });
+
+ setDrawings(drawingsData);
+ toast.success(t("page.drawingLoadSuccess"));
+
+ // 동기화 상태 체크
+ checkPendingSync();
+
+ } catch (err) {
+ console.error("도면 로드 실패:", err);
+ setError(err instanceof Error ? err.message : t("page.drawingLoadError"));
+ toast.error(t("page.drawingLoadError"));
+ } finally {
+ setIsRefreshing(false);
+ }
+ }, [projNo, vendorInfo, t, checkPendingSync]);
+
+ // 상세도면 목록 로드 (V2 API 사용)
+ const loadDetailDrawings = useCallback(async () => {
+ if (!selectedDrawing) {
+ setDetailDrawings([]);
+ setSelectedDetail(null);
+ return;
+ }
+
+ try {
+ setIsLoadingDetails(true);
+ // V2: 로컬 임시 저장 건 포함 조회
+ const data = await fetchDetailDwgReceiptListV2({
+ project: selectedDrawing.ProjectNo,
+ drawingNo: selectedDrawing.DrawingNo,
+ discipline: selectedDrawing.Discipline,
+ drawingKind: selectedDrawing.DrawingKind,
+ userId: vendorInfo?.userId || "",
+ });
+ setDetailDrawings(data);
+
+ if (data.length > 0) {
+ setSelectedDetail(data[0]);
+ } else {
+ setSelectedDetail(null);
+ }
+
+ // 동기화 상태 체크
+ checkPendingSync();
+
+ } catch (error) {
+ console.error("상세도면 로드 실패:", error);
+ toast.error(t("detailDialog.detailLoadError"));
+ setDetailDrawings([]);
+ setSelectedDetail(null);
+ } finally {
+ setIsLoadingDetails(false);
+ }
+ }, [selectedDrawing, vendorInfo, t, checkPendingSync]);
+
+ // 파일 목록 로드 (V2 API 사용)
+ const loadFiles = useCallback(async () => {
+ if (!selectedDetail) {
+ setFiles([]);
+ return;
+ }
+
+ try {
+ setIsLoadingFiles(true);
+ // V2: 로컬 임시 파일 포함 조회
+ const data = await fetchFileInfoListV2(selectedDetail.UploadId);
+ setFiles(data);
+
+ // 동기화 상태 체크
+ checkPendingSync();
+
+ } catch (error) {
+ console.error("파일 목록 로드 실패:", error);
+ toast.error(t("detailDialog.fileLoadError"));
+ setFiles([]);
+ } finally {
+ setIsLoadingFiles(false);
+ }
+ }, [selectedDetail, t, checkPendingSync]);
+
+ // 동기화 완료 핸들러
+ const handleSyncComplete = useCallback(() => {
+ checkPendingSync();
+ if (selectedDrawing) loadDetailDrawings();
+ if (selectedDetail) loadFiles();
+ }, [checkPendingSync, selectedDrawing, selectedDetail, loadDetailDrawings, loadFiles]);
+
+ // 초기 데이터 로드
+ useEffect(() => {
+ loadInitialData();
+ }, [loadInitialData]);
+
+ // 프로젝트 변경 시 자동 검색
+ useEffect(() => {
+ if (projNo && vendorInfo) {
+ loadDrawings();
+ }
+ }, [projNo, vendorInfo, loadDrawings]);
+
+ // 선택된 도면 변경 시 상세도면 로드
+ useEffect(() => {
+ loadDetailDrawings();
+ }, [selectedDrawing, loadDetailDrawings]);
+
+ // 선택된 상세도면 변경 시 파일 목록 로드
+ useEffect(() => {
+ loadFiles();
+ }, [selectedDetail, loadFiles]);
+
+ const handleDrawingClick = (drawing: UnifiedDwgReceiptItem) => {
+ setSelectedDrawing(drawing);
+ };
+
+ const handleSearch = () => {
+ loadDrawings();
+ };
+
+ const handleRefresh = () => {
+ loadDrawings();
+ };
+
+ const handleRefreshDetails = () => {
+ loadDetailDrawings();
+ };
+
+ // 완료 핸들러들
+ const handleBulkUploadComplete = () => {
+ loadDrawings();
+ checkPendingSync();
+ };
+ const handleAddComplete = () => {
+ setAddDialogOpen(false);
+ loadDetailDrawings();
+ checkPendingSync();
+ };
+ const handleUploadComplete = () => {
+ setUploadFilesDialogOpen(false);
+ loadFiles();
+ checkPendingSync();
+ };
+
+ const handleDeleteFile = async (file: FileInfoItem) => {
+ if (!confirm(lng === "ko" ? "정말로 파일을 삭제하시겠습니까?" : "Are you sure you want to delete this file?")) return;
+
+ try {
+ const result = await deleteLocalFile(file.FileId);
+ if (result.success) {
+ toast.success(lng === "ko" ? "파일이 삭제되었습니다." : "File deleted.");
+ loadFiles();
+ checkPendingSync();
+ } else {
+ throw new Error(result.error);
+ }
+ } catch (e) {
+ console.error("File delete failed", e);
+ toast.error(lng === "ko" ? "파일 삭제 실패" : "File delete failed");
+ }
+ };
+
+ const handleDownload = async (file: FileInfoItem) => {
+ try {
+ toast.info(t("detailDialog.downloadPreparing"));
+ const response = await fetch("/api/dolce/download", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ fileId: file.FileId,
+ userId: file.CreateUserId,
+ fileName: file.FileName,
+ }),
+ });
+
+ if (!response.ok) throw new Error(t("detailDialog.downloadError"));
+
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = file.FileName;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ toast.success(t("detailDialog.downloadSuccess"));
+ } catch (error) {
+ console.error("파일 다운로드 실패:", error);
+ toast.error(t("detailDialog.downloadError"));
+ }
+ };
+
+ // 필터 로직
+ const filteredDrawings = useMemo(() => {
+ let result = drawings.filter((drawing) => {
+ if (drawingNo && !drawing.DrawingNo.toLowerCase().includes(drawingNo.toLowerCase())) return false;
+ if (drawingName && !drawing.DrawingName.toLowerCase().includes(drawingName.toLowerCase())) return false;
+ if (discipline && !drawing.Discipline?.toLowerCase().includes(discipline.toLowerCase())) return false;
+ if (manager && !drawing.Manager.toLowerCase().includes(manager.toLowerCase()) &&
+ !drawing.ManagerENM?.toLowerCase().includes(manager.toLowerCase())) return false;
+ return true;
+ });
+
+ if (vendorInfo?.drawingKind === "B4" && documentType !== "ALL") {
+ result = result.filter((drawing) => {
+ if (drawing.DrawingKind !== "B4") return false;
+ const gttDrawing = drawing as { DrawingMoveGbn?: string };
+ if (documentType === "SHI_INPUT") return gttDrawing.DrawingMoveGbn === "도면제출";
+ else if (documentType === "GTT_DELIVERABLES") return gttDrawing.DrawingMoveGbn === "도면입수";
+ return true;
+ });
+ }
+ return result;
+ }, [drawings, drawingNo, drawingName, discipline, manager, vendorInfo?.drawingKind, documentType]);
+
+ const getDetailDrawingId = (detail: DetailDwgReceiptItem) => `${detail.RegisterId}_${detail.UploadId}`;
+ const getDrawingId = (drawing: UnifiedDwgReceiptItem) => `${drawing.ProjectNo}_${drawing.DrawingNo}_${drawing.Discipline}`;
+
+ const canAddDetailDrawing = vendorInfo && (
+ vendorInfo.drawingKind === "B3" ||
+ (vendorInfo.drawingKind === "B4" && selectedDrawing && 'DrawingMoveGbn' in selectedDrawing && selectedDrawing.DrawingMoveGbn === "도면입수")
+ );
+
+ // 파일 리스트 컬럼 정의
+ const fileColumns = createFileListColumns({
+ onDownload: handleDownload,
+ onDelete: handleDeleteFile,
+ lng
+ });
+
+ if (isLoading) {
+ return (
+ <div className="space-y-4">
+ <Card><CardHeader><Skeleton className="h-8 w-48" /></CardHeader><CardContent><Skeleton className="h-32 w-full" /></CardContent></Card>
+ <Card><CardHeader><Skeleton className="h-8 w-48" /></CardHeader><CardContent><Skeleton className="h-96 w-full" /></CardContent></Card>
+ </div>
+ );
+ }
+
+ return (
+ <div className="space-y-4 max-w-full overflow-x-hidden h-full flex flex-col">
+ {error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
+
+ {/* 헤더 및 Sync 컨트롤 */}
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-xl font-bold flex items-center gap-2">
+ Dolce Upload V3 <span className="text-xs font-normal bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 px-2 py-1 rounded-full">Sync Enabled</span>
+ </h2>
+ </div>
+ <div className="flex items-center gap-2">
+ <Button
+ size="sm"
+ variant={pendingSyncCount > 0 ? "default" : "outline"}
+ onClick={() => setSyncDialogOpen(true)} // 다이얼로그 열기
+ disabled={pendingSyncCount === 0 || !projNo}
+ >
+ <Cloud className="h-4 w-4 mr-2" />
+ Send to SHI
+ </Button>
+ </div>
+ </div>
+
+ {!projNo && <Alert><InfoIcon className="h-4 w-4" /><AlertDescription>{t("page.selectProject")}</AlertDescription></Alert>}
+
+ {/* 필터 카드 */}
+ <Card className="flex-shrink-0">
+ <CardHeader className="py-3"><CardTitle className="text-base">{t("filter.title")}</CardTitle></CardHeader>
+ <CardContent className="py-3">
+ <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-3">
+ <div className="space-y-1">
+ <Label className="text-xs">{t("filter.project")}</Label>
+ <Select value={projNo} onValueChange={setProjNo}>
+ <SelectTrigger className="h-8"><SelectValue placeholder={t("filter.projectPlaceholder")} /></SelectTrigger>
+ <SelectContent>
+ {projects.map((p) => <SelectItem key={p.code} value={p.code}>{p.code} - {p.name}</SelectItem>)}
+ </SelectContent>
+ </Select>
+ </div>
+ {/* 기타 필터들 */}
+ <div className="space-y-1"><Label className="text-xs">{t("filter.drawingNo")}</Label><Input className="h-8" value={drawingNo} onChange={(e) => setDrawingNo(e.target.value)} /></div>
+ <div className="space-y-1"><Label className="text-xs">{t("filter.drawingName")}</Label><Input className="h-8" value={drawingName} onChange={(e) => setDrawingName(e.target.value)} /></div>
+ <div className="space-y-1"><Label className="text-xs">{t("filter.discipline")}</Label><Input className="h-8" value={discipline} onChange={(e) => setDiscipline(e.target.value)} /></div>
+ <div className="space-y-1"><Label className="text-xs">{t("filter.manager")}</Label><Input className="h-8" value={manager} onChange={(e) => setManager(e.target.value)} /></div>
+ {vendorInfo?.drawingKind === "B4" && (
+ <div className="space-y-1">
+ <Label className="text-xs">{t("filter.documentType")}</Label>
+ <Select value={documentType} onValueChange={(v) => setDocumentType(v as DocumentType)}>
+ <SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
+ <SelectContent>
+ <SelectItem value="ALL">{t("filter.documentTypeAll")}</SelectItem>
+ <SelectItem value="GTT_DELIVERABLES">{t("filter.documentTypeGttDeliverables")}</SelectItem>
+ <SelectItem value="SHI_INPUT">{t("filter.documentTypeSHIInput")}</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ )}
+ </div>
+ <div className="flex gap-2 mt-3 justify-end">
+ <Button size="sm" onClick={handleSearch} disabled={!projNo || isRefreshing}><Search className="h-4 w-4 mr-2" />{t("filter.searchButton")}</Button>
+ {vendorInfo?.drawingKind === "B4" && (
+ <Button size="sm" onClick={() => setBulkUploadDialogOpen(true)} disabled={!projNo || isRefreshing}>
+ <Upload className="h-4 w-4 mr-2" />{t("filter.bulkUploadButton")}
+ </Button>
+ )}
+ <Button size="sm" variant="outline" onClick={handleRefresh} disabled={!projNo || isRefreshing}><RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} /></Button>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 메인 컨텐츠 영역 */}
+ <Card className="flex-shrink-0" style={{ minHeight: "400px" }}>
+ <CardHeader className="py-3"><CardTitle className="text-base">{t("drawingList.title")}</CardTitle></CardHeader>
+ <CardContent className="p-0">
+ <DrawingListTableV2
+ columns={
+ vendorInfo?.drawingKind === "B4"
+ ? (createGttDrawingListColumns({ documentType, lng, t }) as unknown as typeof drawingListColumns as any)
+ : (drawingListColumns(lng, t) as unknown as typeof drawingListColumns as any)
+ }
+ data={filteredDrawings}
+ onRowClick={handleDrawingClick}
+ selectedRow={selectedDrawing || undefined}
+ getRowId={getDrawingId}
+ maxHeight="400px"
+ minHeight="300px"
+ />
+ </CardContent>
+ </Card>
+
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 flex-1 min-h-0">
+ <Card className="flex flex-col min-h-0">
+ <CardHeader className="flex-row items-center justify-between py-3">
+ <CardTitle className="text-base">{t("detailDialog.detailListTitle")}</CardTitle>
+ <div className="flex gap-2">
+ <Button variant="outline" size="sm" onClick={handleRefreshDetails} disabled={!selectedDrawing}><RefreshCw className={`h-4 w-4 ${isLoadingDetails ? "animate-spin" : ""}`} /></Button>
+ {canAddDetailDrawing && <Button size="sm" onClick={() => setAddDialogOpen(true)} disabled={!selectedDrawing}><Plus className="h-4 w-4 mr-2" />{t("detailDialog.addDetailButton")}</Button>}
+ </div>
+ </CardHeader>
+ <CardContent className="p-0 flex-1">
+ <DrawingListTableV2
+ columns={createDetailDrawingColumns(lng, t) as any}
+ data={detailDrawings}
+ onRowClick={setSelectedDetail as any}
+ selectedRow={selectedDetail || undefined}
+ getRowId={getDetailDrawingId}
+ maxHeight="400px"
+ minHeight="300px"
+ />
+ </CardContent>
+ </Card>
+
+ <Card className="flex flex-col min-h-0">
+ <CardHeader className="flex-row items-center justify-between py-3">
+ <CardTitle className="text-base">{t("detailDialog.fileListTitle")}</CardTitle>
+ {selectedDetail && canAddDetailDrawing && (
+ <Button size="sm" onClick={() => setUploadFilesDialogOpen(true)}><Upload className="h-4 w-4 mr-2" />{t("detailDialog.uploadFilesButton")}</Button>
+ )}
+ </CardHeader>
+ <CardContent className="p-0 flex-1">
+ <DrawingListTableV2
+ columns={fileColumns as any}
+ data={files}
+ maxHeight="400px"
+ minHeight="300px"
+ />
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* 다이얼로그 영역 */}
+ {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && (
+ <B4BulkUploadDialogV3Sync
+ open={bulkUploadDialogOpen}
+ onOpenChange={setBulkUploadDialogOpen}
+ projectNo={projNo}
+ userId={vendorInfo.userId}
+ userName={vendorInfo.userName}
+ userEmail={vendorInfo.email}
+ vendorCode={vendorInfo.vendorCode}
+ onUploadComplete={handleBulkUploadComplete}
+ lng={lng}
+ />
+ )}
+
+ {vendorInfo && selectedDrawing && (
+ <AddAndModifyDetailDrawingDialogV2
+ open={addDialogOpen}
+ onOpenChange={setAddDialogOpen}
+ drawing={selectedDrawing}
+ vendorCode={vendorInfo.vendorCode}
+ userId={vendorInfo.userId}
+ userName={vendorInfo.userName}
+ userEmail={vendorInfo.email}
+ onComplete={handleAddComplete}
+ drawingKind={vendorInfo.drawingKind}
+ lng={lng}
+ />
+ )}
+
+ {vendorInfo && selectedDetail && (
+ <UploadFilesToDetailDialogV2
+ open={uploadFilesDialogOpen}
+ onOpenChange={setUploadFilesDialogOpen}
+ uploadId={selectedDetail.UploadId}
+ drawingNo={selectedDetail.DrawingNo}
+ revNo={selectedDetail.DrawingRevNo}
+ // 추가된 props
+ drawingName={selectedDrawing?.DrawingName}
+ discipline={selectedDrawing?.Discipline}
+ registerKind={selectedDetail.RegisterKind}
+
+ userId={vendorInfo.userId}
+ projectNo={projNo}
+ vendorCode={vendorInfo.vendorCode} // 추가: Vendor Code
+ onUploadComplete={handleUploadComplete}
+ lng={lng}
+ />
+ )}
+
+ {/* 동기화 다이얼로그 */}
+ {vendorInfo && projNo && (
+ <SyncItemsDialog
+ open={syncDialogOpen}
+ onOpenChange={setSyncDialogOpen}
+ projectNo={projNo}
+ userId={vendorInfo.userId}
+ vendorCode={vendorInfo.vendorCode}
+ onSyncComplete={handleSyncComplete}
+ lng={lng}
+ />
+ )}
+ </div>
+ );
+}
diff --git a/app/[lng]/partners/(partners)/dolce-upload-v3/page.tsx b/app/[lng]/partners/(partners)/dolce-upload-v3/page.tsx
new file mode 100644
index 00000000..f62f486b
--- /dev/null
+++ b/app/[lng]/partners/(partners)/dolce-upload-v3/page.tsx
@@ -0,0 +1,49 @@
+import { Suspense } from "react";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import DolceUploadPageV3 from "./dolce-upload-page-v3";
+import { Shell } from "@/components/shell";
+
+export const metadata = {
+ title: "조선 벤더문서 업로드(DOLCE) V3",
+ description: "조선 설계문서 업로드 및 관리 - 오프라인 동기화 지원",
+};
+
+function DolceUploadSkeleton() {
+ return (
+ <div className="space-y-4">
+ <Card><CardHeader><Skeleton className="h-8 w-48" /></CardHeader><CardContent><Skeleton className="h-32 w-full" /></CardContent></Card>
+ <Card><CardHeader><Skeleton className="h-8 w-48" /></CardHeader><CardContent><Skeleton className="h-96 w-full" /></CardContent></Card>
+ </div>
+ );
+}
+
+export default async function DolceUploadPageWrapper({
+ params,
+ searchParams,
+}: {
+ params: Promise<{ lng: string }>;
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+ const { lng } = await params;
+ const resolvedParams = await searchParams;
+
+ return (
+ <Shell variant="fullscreen">
+ <div className="flex items-center justify-between flex-shrink-0">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ {lng === "ko" ? "DOLCE 도면 업로드 V3 (동기화)" : "DOLCE Drawing Upload V3 (Sync)"}
+ </h2>
+ <p className="text-muted-foreground">
+ {lng === "ko" ? "임시 저장 및 서버 동기화 기능을 지원합니다." : "Supports temporary save and server synchronization."}
+ </p>
+ </div>
+ </div>
+
+ <Suspense fallback={<DolceUploadSkeleton />}>
+ <DolceUploadPageV3 searchParams={resolvedParams} />
+ </Suspense>
+ </Shell>
+ );
+}